﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="DownloadTaskManager.cs" company="Mar3ek">
//   Copyright (c) Mar3ek 2012
// </copyright>
// <summary>
//   Queue of downloads.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace DownloaderCore
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Threading;
    using System.Threading.Tasks;

    /// <summary> Queue of downloads. </summary>
    /// <remarks> Mar3ek, 11/3/2012. </remarks>
    public class DownloadTaskManager : IDisposable
    {
        /// <summary> The running tasks. </summary>
        private readonly List<Tuple<DownloadTask, CancellationTokenSource>> runningTasks = new List<Tuple<DownloadTask, CancellationTokenSource>>();

        /// <summary> Synchronization primitive for maintaining parallel tasks cap. </summary>
        private readonly AutoResetEvent maxParallelTasksSynchro = new AutoResetEvent(false);

        /// <summary> The ordered list of download tasks. </summary>
        private readonly List<DownloadTask> downloadTasksOrdered = new List<DownloadTask>();

        /// <summary> The download tasks. </summary>
        private BlockingCollection<DownloadTask> downloadTaskQueue = new BlockingCollection<DownloadTask>(new ConcurrentQueue<DownloadTask>());

        /// <summary> Cancellation token source for processing thread. </summary>
        private CancellationTokenSource queueCancellationSource = new CancellationTokenSource();

        /// <summary> The maximum parallel tasks number. </summary>
        private int maximumParallelCap = 1;

        /// <summary> Indicates whether this instance has already been disposed. </summary>
        private bool isDisposed;

        /// <summary> Gets the download thread. </summary>
        public Task DownloadThread { get; private set; }

        /// <summary> Gets or sets the number of tasks that will be processed concurrently. </summary>
        /// <value> The max number of concurrent tasks. </value>
        public int MaximumParallelTasks
        {
            get
            {
                return this.maximumParallelCap;
            }

            set
            {
                if (value < 1)
                {
                    throw new InvalidOperationException("Maximum parallel tasks count must be at least 1.");
                }

                if (value == this.maximumParallelCap)
                {
                    return;
                }

                System.Diagnostics.Debug.WriteLine("Setting parallel cap - " + value);
                if (value < this.maximumParallelCap)
                {
                    this.maximumParallelCap = value;
                }
                else
                {
                    this.maximumParallelCap = value;
                    this.maxParallelTasksSynchro.Set();
                }
            }
        }

        /// <summary> Gets the ordered list of download tasks. </summary>
        /// <value> The download tasks. </value>
        public ReadOnlyCollection<DownloadTask> DownloadTasks
        {
            get
            {
                return new ReadOnlyCollection<DownloadTask>(this.downloadTasksOrdered);
            }
        }

        /// <summary> Starts a download task. </summary>
        /// <remarks> Mar3ek, 11/6/2012. </remarks>
        /// <param name="task"> The task to start. </param>
        public void StartTask(DownloadTask task)
        {
            if (task == null || !this.downloadTasksOrdered.Contains(task) || task.IsActive || task.IsFinished)
            {
                return;
            }

            this.RunTask(task, new CancellationTokenSource());
        }

        /// <summary> Verifies the specified download task link. </summary>
        /// <remarks> Mar3ek, 11/6/2012. </remarks>
        /// <param name="task"> The task to verify. </param>
        public void VerifyTask(DownloadTask task)
        {
            Task.Run(
                async () =>
                    {
                        var token = new CancellationToken(false);
                        await task.VerifyDownloadLink(token);

                        this.RemoveRunningTask(task, token.IsCancellationRequested);
                    });
        }

        /// <summary> Starts the download queue. </summary>
        /// <remarks> Mar3ek, 11/3/2012. </remarks>
        public void StartQueue()
        {
            System.Diagnostics.Debug.WriteLine("Starting queue");
            this.DownloadThread = Task.Run(() => this.ProcessDownloadQueueAsync(this.queueCancellationSource.Token), this.queueCancellationSource.Token);
        }

        /// <summary> Stops the download queue. </summary>
        /// <remarks> Mar3ek, 11/3/2012. </remarks>
        public void StopQueue()
        {
            System.Diagnostics.Debug.WriteLine("Stopping queue");
            this.RenewQueueCancellationToken();

            while (this.runningTasks.Any())
            {
                this.runningTasks[0].Item2.Cancel();
            }
        }

        /// <summary> Adds a new download task. </summary>
        /// <remarks> Mar3ek, 11/3/2012. </remarks>
        /// <param name="task"> The task. </param>
        public void AddDownloadTask(DownloadTask task)
        {
            this.downloadTasksOrdered.Insert(this.downloadTasksOrdered.Count, task);
            this.downloadTaskQueue.Add(task);
        }

        /// <summary> Removes the download task described by task. </summary>
        /// <remarks> Mar3ek, 11/4/2012. </remarks>
        /// <param name="task"> The task to remove. </param>
        public void RemoveDownloadTask(DownloadTask task)
        {
            this.queueCancellationSource.Cancel(false);
            this.RenewQueueCancellationToken();

            var removed = this.downloadTasksOrdered.Remove(task);

            if (!removed)
            {
                // the task was not found
                return;
            }

            var runningTask = this.runningTasks.FirstOrDefault(pair => pair.Item1 == task);

            if (runningTask != null)
            {
                runningTask.Item2.Cancel(false);
            }

            var waitingTasks = this.downloadTasksOrdered.Where(t => this.runningTasks.All(pair => pair.Item1 != t));
            this.downloadTaskQueue = new BlockingCollection<DownloadTask>(new ConcurrentQueue<DownloadTask>(waitingTasks));
            this.StartQueue();
        }

        /// <summary> Moves the specified download task to another place in the queue. </summary>
        /// <remarks> Mar3ek, 11/4/2012. </remarks>
        /// <param name="task"> The task. </param>
        /// <param name="newPosition"> The new position. </param>
        public void MoveDownloadTask(DownloadTask task, int newPosition)
        {
            this.queueCancellationSource.Cancel(false);
            this.RenewQueueCancellationToken();

            if (this.MoveTaskInternal(task, newPosition))
            {
                return;
            }

            this.StartQueue();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged
        /// resources.
        /// </summary>
        /// <remarks> Mar3ek, 11/6/2012. </remarks>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary> Internal implementation of the task moving process. </summary>
        /// <remarks> Mar3ek, 11/4/2012. </remarks>
        /// <param name="task"> The task to move. </param>
        /// <param name="newPosition"> The new position. </param>
        /// <returns> true if it succeeds, false if it fails. </returns>
        private bool MoveTaskInternal(DownloadTask task, int newPosition)
        {
            var oldPosition = this.downloadTasksOrdered.IndexOf(task);

            if (oldPosition == newPosition)
            {
                // moving to the original place
                return true;
            }

            var removed = this.downloadTasksOrdered.Remove(task);

            if (!removed)
            {
                // task was not present in the list
                return true;
            }

            if (oldPosition > newPosition)
            {
                // moving forward
                this.downloadTasksOrdered.Insert(newPosition, task);
            }
            else
            {
                // moving backwards
                this.downloadTasksOrdered.Insert(newPosition + 1, task);
            }

            var waitingTasks = this.downloadTasksOrdered.Where(t => this.runningTasks.All(pair => pair.Item1 != t));
            this.downloadTaskQueue = new BlockingCollection<DownloadTask>(new ConcurrentQueue<DownloadTask>(waitingTasks));
            return false;
        }

        /// <summary> Process the download queue asynchronously. </summary>
        /// <remarks> Mar3ek, 11/3/2012. </remarks>
        /// <param name="token"> The cancellation token to use. </param>
        private void ProcessDownloadQueueAsync(CancellationToken token)
        {
            while (true)
            {
                lock (this.runningTasks)
                {
                    var currentDownload = this.downloadTaskQueue.Take(token);
                    System.Diagnostics.Debug.WriteLine("Dequed - " + currentDownload.Name);
                    var downloadCancellationSource = new CancellationTokenSource();

                    System.Diagnostics.Debug.WriteLine("Entering monitor - " + currentDownload.Name);
                    while (this.runningTasks.Count >= this.maximumParallelCap)
                    {
                        System.Diagnostics.Debug.WriteLine("Entering sleep - " + currentDownload.Name);
                        this.maxParallelTasksSynchro.WaitOne(500);
                        if (token.IsCancellationRequested)
                        {
                            System.Diagnostics.Debug.WriteLine("Sleep cancelled - " + currentDownload.Name);
                            return;
                        }

                        System.Diagnostics.Debug.WriteLine("Exiting sleep - " + currentDownload.Name);
                    }

                    this.RunTask(currentDownload, downloadCancellationSource);
                }
            }
        }

        /// <summary> Executes the download task. </summary>
        /// <remarks> Mar3ek, 11/4/2012. </remarks>
        /// <param name="task"> The download. </param>
        /// <param name="tokenSource"> The cancellation token to use. </param>
        private void RunTask(DownloadTask task, CancellationTokenSource tokenSource)
        {
            System.Diagnostics.Debug.WriteLine("Task enqued - " + task.Name);
            this.runningTasks.Add(new Tuple<DownloadTask, CancellationTokenSource>(task, tokenSource));
            System.Diagnostics.Debug.WriteLine("Exiting monitor - " + task.Name);

            Task.Run(
                async () =>
                    {
                        await task.Start(tokenSource.Token);

                        this.RemoveRunningTask(task, tokenSource.Token.IsCancellationRequested);
                    });
        }

        /// <summary> Removes the running task described by taskToRemove. </summary>
        /// <remarks> Mar3ek, 11/3/2012. </remarks>
        /// <param name="taskToRemove"> The task to remove. </param>
        /// <param name="isCancelled"> Specifies whether the task finished successfully or was cancelled. </param>
        private void RemoveRunningTask(DownloadTask taskToRemove, bool isCancelled)
        {
            if (!this.runningTasks.Remove(this.runningTasks.FirstOrDefault(t => t.Item1 == taskToRemove)))
            {
                return;
            }

            if (isCancelled)
            {
                return;
            }

            System.Diagnostics.Debug.WriteLine("Task finished - " + taskToRemove.Name);
            this.downloadTasksOrdered.Remove(taskToRemove);
            System.Diagnostics.Debug.WriteLine("Setting synchro - " + taskToRemove.Name);
            this.maxParallelTasksSynchro.Set();
        }

        /// <summary> Renews queue cancellation token. </summary>
        /// <remarks> Mar3ek, 11/4/2012. </remarks>
        private void RenewQueueCancellationToken()
        {
            this.queueCancellationSource.Cancel(false);
            this.queueCancellationSource.Dispose();
            this.queueCancellationSource = new CancellationTokenSource();
        }

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged
        /// resources.
        /// </summary>
        /// <remarks> Mar3ek, 11/6/2012. </remarks>
        /// <param name="disposing"> true to release both managed and unmanaged resources; false to
        /// release only unmanaged resources.
        /// </param>
        private void Dispose(bool disposing)
        {
            if (this.isDisposed)
            {
                return;
            }

            if (disposing)
            {
                this.downloadTaskQueue.Dispose();
                this.queueCancellationSource.Dispose();
                this.maxParallelTasksSynchro.Dispose();
            }

            this.isDisposed = true;
        }
    }
}
